package wiiusej.connection;

import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.spi.RootLogger;

import sun.security.action.GetLongAction;

import com.sun.swing.internal.plaf.synth.resources.synth;

import wiiusej.WiiUseApi;
import wiiusej.WiiUseApiManager;
import wiiusej.Wiimote;
import wiiusej.apptest.WiimoteEventListener;
import wiiusej.apptest.WiimoteNetListener;
import wiiusej.apptest.activityTasks.EnergyTransmisionTask;
import wiiusej.apptest.activityTasks.Spell;
import wiiusej.apptest.statistics.AccelerationPaquetList;
import wiiusej.hardwareControllers.WiimoteSynchronizer;

import wiiusej.wiiusejevents.GenericEvent;
import wiiusej.wiiusejevents.utils.WiiUseApiListener;
import wiiusej.wiiusejevents.utils.WiimoteBasicListener;
import wiiusej.wiiusejevents.utils.WiimoteListener;
import wiiusej.wiiusejevents.wiiuseapievents.DisconnectionEvent;
import wiiusej.wiiusejevents.wiiuseapievents.WiiUseApiEvent;



public class ConnectionManager extends Thread implements WiiUseApiListener{
	
	//Logger	
	private static Logger fileLogger;
	
	//producer-consumer queue for Wiimote-WiiUseApimanager COM
	private static BlockingQueue<HashMap<Integer, BlockingQueue<GenericEvent>>> toAPI;	
	private static BlockingQueue<WiiUseApiEvent> fromAPI;
	
	//producer-consumer queue for Wiimote/Listener-Synchonizer COM
	private static ConcurrentLinkedQueue<AccelerationPaquetList> syncQueue = 
		new ConcurrentLinkedQueue<AccelerationPaquetList>();
	
	//Synchronizer
	private static WiimoteSynchronizer synchronizer;
	
	//To Com client queue
	private static BlockingQueue<Spell> eneergyToComClient = new LinkedBlockingDeque<Spell>();
	
	//EXECUTOR
	private static ExecutorService exe = null;
	
	private static HashMap<Integer, Wiimote> wiimotes;
	private static HashMap<Integer, BlockingQueue<GenericEvent>> wiimoteComChannels 
		= new HashMap<Integer, BlockingQueue<GenericEvent>>();
	
	private static BlockingQueue<WiiUseApiEvent> fromPostman;
	
	private static HashMap<Integer, WiimoteBasicListener> wiimoteListeners;
	private static WiiUseApiManager manager;
	//private static WiiUseApi wiiuse;
	
	private static ApiConnection connector;
	
	
	private boolean leave = false;
	private AtomicBoolean running = new AtomicBoolean(false);
	private static int connected;
	
	public static int WIIUSE_STACK_UNKNOWN = 0;
	public static int WIIUSE_STACK_MS = 1;
	public static int WIIUSE_STACK_BLUESOLEIL = 2;
	public static int INITIAL_WIIMOTES_DEVICES = 8;
	public static int CONNECTION_TIMEOUT_SEC = 3;
	
	//Object to share information with the controller (WiiUseApiManager)
	//private final ShareConnectionCell sharedConnectionCell;
	
	//the barrier
	//private static CyclicBarrier barrier = null;
	
	private final ConnectionManager me;
	
	//Run Mode
	private static boolean enableListeners; 
	
	//Wiimote Timeots Configuration in milliseconds
	short normalTimeout = 15;
	short expTimeout = 15;
	
	/**
	 * Constructor
	 *	  
	 * @param manager
	 *            get a reference to the system controller.
	 * @param eneergyToComClient 
	 * @param apiToConnection 
	 * @param postmanToNetListener 
	 * @param enableWiimoteListeners 
	 * 
	 */
	
	public ConnectionManager(ExecutorService exe, WiiUseApiManager manager, BlockingQueue<HashMap<Integer, BlockingQueue<GenericEvent>>> connectionToAPI, 
			BlockingQueue<Spell> eneergyToComClient, BlockingQueue<WiiUseApiEvent> apiToConnection,
			BlockingQueue<WiiUseApiEvent> postmanToNetListener, boolean enableWiimoteListeners){
		
		
		me = this;
		
		this.eneergyToComClient = eneergyToComClient;
		
		this.toAPI = connectionToAPI;
		this.fromAPI = apiToConnection;
		this.fromPostman = postmanToNetListener;
		
		wiimotes = new HashMap<Integer, Wiimote>();
		wiimoteListeners = new HashMap<Integer, WiimoteBasicListener>();
		
		this.manager = manager;
		this.connector = (ApiConnection) manager;
		
		//this.sharedConnectionCell = cell;
		
		//barrier
		//this.barrier = barrier;
		
		this.enableListeners = enableWiimoteListeners;
		
		this.exe = exe;		
		
		//Logger init
		PropertyConfigurator.configure("log4j.properties");
		fileLogger = Logger.getLogger("connectionManager");
		
		fileLogger.debug("Thread Name: " + this.getName() + "ID: " + this.getId());
		
		
		//Set connectionManager in controller (WiiUseApiManager)
		//manager.setConnectionManager(this);
		
		//we create the listeners for the connected wiimotes
		getWiimotesPrivate(INITIAL_WIIMOTES_DEVICES, true, 
				false, WIIUSE_STACK_UNKNOWN, this);
		
		
		fileLogger.debug("Wiimote Structure: " + wiimotes.size());
		
		if(enableListeners){
			createListeners(wiimotes);
		}else{
			setNetworkMode(wiimotes);			
		}
		
	}

	/**
	 * Get wiimotes. Load library if necessary. Connect to wiimotes if
	 * necessary. Start polling if necessary. Return an array with the connected
	 * wiimotes.
	 * 
	 * @param nb
	 *            try to connect nb wiimotes.
	 * @param rumble
	 *            make the connected wiimotes rumble.*
	 * @param forceStackType
	 *            true if we want to force the stack type.
	 * @param stackType
	 *            the stack type : WiiUseApiManager.WIIUSE_STACK_UNKNOWN or
	 *            WiiUseApiManager.WIIUSE_STACK_MS or
	 *            WiiUseApiManager.WIIUSE_STACK_BLUESOLEIL
	 * 
	 * @return an array with connected wiimotes or NULL.
	 */
	private synchronized static HashMap<Integer, Wiimote> getWiimotesPrivate(int nb,
			boolean rumble, boolean forceStackType, int stackType, ConnectionManager me) {
		
		// connect wiimotes.		
		int nbWiimotes = initConnection(nb, forceStackType, stackType);		
	
		fileLogger.debug("Discovered devices: " + nbWiimotes);
		//Once we have discovered the connected devices we create the internal 
		//data structure
		initStructure(nbWiimotes);	
		
		/*
		if (manager.connected == 0) {// no wiimote connected
			// return empty array
			return new Wiimote[0];
		}

		if (!manager.isAlive())// start wiiuseJ polling
			manager.start();

		manager.semaphore.release(1);

		return manager.wiimotes;*/
		
		fileLogger.debug("Starting Connector Manager Run");
		//start the thread
		//me.start();
		
		return wiimotes;
	}
	
	
	
	/**
	 * Connect wiimote and get the number of wiimotes connected. Supposed to be
	 * used once.
	 * 
	 * @param nb
	 *            try to connect nb wiimotes
	 * @param rumble
	 *            make the connected wiimotes rumble
	 * @param forceStackType
	 *            true if we want to force the stack type.
	 * @param stackType
	 *            the stack type : WiiUseApiManager.WIIUSE_STACK_UNKNOWN or
	 *            WiiUseApiManager.WIIUSE_STACK_MS or
	 *            WiiUseApiManager.WIIUSE_STACK_BLUESOLEIL
	 * @return 0 if nothing connected or the number of wiimotes connected.
	 */
	private static int initConnection(int nbPossibleWiimotes, boolean forceStack, int stackType) {

		int nbWiimotesFound;
		
		//Init C data Structure to allocate wiimote
		initApiStructure(nbPossibleWiimotes);
		
		//Select the Bluetoot stsack type
		bluetootStack(forceStack, stackType);
		
		//Try to find the connected wiimotes
		nbWiimotesFound = connector.findWiimotes(nbPossibleWiimotes, CONNECTION_TIMEOUT_SEC);
		
		//Connect the discovered devices
		return connected = connector.connectDiscoveredWiimotes(nbWiimotesFound);
		
	}
	
	private static void initStructure(int nbWiimotes){
		
		//Creation of the Wiimote data structure which represents the connected devices.
		
		for(int i = 0; i < nbWiimotes; i++){			
			//Wee need to pass an unique identifier and a manager (controller)
			
			if(manager == null){
				fileLogger.debug("WARNING --> Building a Wiimote with Manager NULL");
			}
			
			
			BlockingQueue<GenericEvent> wiimoteQueue = new LinkedBlockingDeque<GenericEvent>();
			
			int id = connector.getUnId(i);
			
			//Wiimote object creation
			Wiimote wim = new Wiimote(id, manager, wiimoteQueue, syncQueue);
			wim.start();			
			
			//send the queue to postmant to stablish the connection
			wiimoteComChannels.put(id, wiimoteQueue);			
						
			//We add the wiimote to the HashMap
			wiimotes.put(wim.getWiimoteId(), wim);			
			
		}
		
		if(!wiimotes.isEmpty()){			
			sendActivationFeedback(wiimotes);
		}
		
		fileLogger.debug("Discovered Wiimotes: " + wiimotes);		
				
	}
	
	private static void sendActivationFeedback(HashMap<Integer, Wiimote> wiimotes){
		fileLogger.debug("Seding Activation To WiimotesIds: " + wiimotes);		
		
		Iterator it = wiimotes.entrySet().iterator();		
		while(it.hasNext()){
			Map.Entry entry = (Map.Entry) it.next();
			Wiimote aux = (Wiimote) entry.getValue();
			
			aux.activateRumble();
			
		}		
	}
		
	/**
	 * Init the C wiimote data structure
	 */	
	private static void initApiStructure(int nbPossibleWiimotes){
		connector.initDataStructure(nbPossibleWiimotes);
	}
	
	/**
	 * Force Bluetoot stack type 
	 * @return 
	 */
	
	private static void bluetootStack(boolean forceStackType, int stackType){
		if(forceStackType){
			connector.setBlueToothstackType(stackType);
		}				
	}
	
	public synchronized void run(){		

		SimpleDateFormat tsFormat = new SimpleDateFormat();
		
		fileLogger.info("");
		fileLogger.debug("RUN METHOD");
		
		fileLogger.info("Wiimotes Connected: " + wiimoteComChannels.size());
		fileLogger.debug("WiimoteChannels: " + wiimoteComChannels.toString());
		
		//We need to send the connected wiimotes to WiiuseAPi
		if(wiimotes.size() > 0){
			
			try {
				fileLogger.debug("Sending info to WiiUseAPIListener - Time: " + 
						tsFormat.format(System.currentTimeMillis()));
				toAPI.put(wiimoteComChannels);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			
			//Create Network listener
			fileLogger.info("Creating Network Listener");
			WiimoteNetListener netListener = new WiimoteNetListener(manager, fromPostman, wiimotes);
			
			
		}
		
		
		//while we have some connected device
		while(true){	
			try {
				WiiUseApiEvent incomingEvent = fromAPI.take();
				
				if(incomingEvent != null){
					fileLogger.info("IncomingEvent: " + incomingEvent.toString());					
				}
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			
			
		}
	}
	
	
		
	/**
	 * Stop thread and shutdown wiiuse Api.
	 */	
	public static void shutdown() {
		
		//WiiUseApiManager manager = getInstance();
		
		//int pastConnected = manager.connected; 
		
		Iterator it = wiimotes.entrySet().iterator();		
		
		while(it.hasNext()){
			Map.Entry entry = (Map.Entry) it.next();
			Wiimote wim = (Wiimote) entry.getValue();
			wim.disconnect();			
		}
		
		/*
		if (manager.connected > 0) {
			for (Wiimote wim : manager.wiimotes) {
				if (wim != null)
					wim.disconnect();
			}
		}*/
		
		/*
		manager.running.set(false);
		if (pastConnected > 0) {
			manager.wiiuse.cleanUp();
		}*/
	}

	@Override
	public void onWiiUseApiEvent(WiiUseApiEvent e) {
		
		if(e.getEventType() == WiiUseApiEvent.DISCONNECTION_EVENT){
			//we have to close the connection of the disconected event.
			connector.closeConnection(e.getWiimoteId());
			
			if(!wiimotes.containsKey(e.getWiimoteId())){
				//upload the wiimote data structure
				wiimotes.remove(e.getWiimoteId());				
			}else{
				fileLogger.debug("Wiimote: " + e.getWiimoteId() + " requested to disconnect " +
						"NOT in wiimotes");
			}			
		}else{
			fileLogger.debug("Received other EventType: " + e.getEventType());
		}
	}
	
	public boolean setWiiUseApiManager(WiiUseApiManager manager){		
		
		if(manager instanceof WiiUseApiManager){			
			this.manager = manager;
			return true;
		}
		return false;		
	}

	
	
	private static void createListeners(HashMap<Integer, Wiimote> wiimotes){
		
		Iterator it = wiimotes.entrySet().iterator();		
		
		fileLogger.debug("Listeners Created");
		
		while(it.hasNext()){
			
			Map.Entry entry = (Map.Entry) it.next();
			Wiimote aux = (Wiimote) entry.getValue();
			
			//WiimoteListener listener = new WiimoteEventListenerRealTime(aux, exe);
			
			//assign Wiimote ID to hashmap listener key
			wiimoteListeners.put(aux.getWiimoteId(), getListener(aux));
			
			fileLogger.debug("Listener Key: " + aux.getWiimoteId());
		}		
		
	}
	
	/*
	 * Deactivate all the incomming events in order to isolate just desired status events
	 */
	private void setNetworkMode(HashMap<Integer, Wiimote> wiimotes) {
		Iterator it = wiimotes.entrySet().iterator();
		
		fileLogger.info("Deactivating Motion/IR/Rumble/Continuous: ");
		while(it.hasNext()){
			Map.Entry entry = (Map.Entry) it.next();
			Wiimote aux = (Wiimote) entry.getValue();
			
			fileLogger.info("*** Wiimote: " + aux.getId());
			aux.deactivateIRTRacking();
			aux.deactivateMotionSensing();
			aux.deactivateRumble();
			aux.deactivateContinuous();
			
		}
		
		setNetworkTimeout(wiimotes);
		
	}
	
	private void setNetworkTimeout(HashMap<Integer, Wiimote> wiimotes) {
		
		Iterator it = wiimotes.entrySet().iterator();
		
		
		while(it.hasNext()){
			Map.Entry entry = (Map.Entry) it.next();
			Wiimote aux = (Wiimote) entry.getValue();
		
			fileLogger.info("Setting Timeouts Wiimote: " + aux.getId() + 
					" Normal: " + normalTimeout + " Expansion: " + expTimeout);
			
			aux.setTimeout(normalTimeout, expTimeout);
		}
		
	}
	
	

	private static WiimoteEventListener getListener(Wiimote aux){
		WiimoteEventListener listener = new WiimoteEventListener(aux, exe, syncQueue, eneergyToComClient);
		return listener;		
	}
	
	@Override
	public String toString(){		
		return Thread.currentThread().getName();
	}
}
